﻿#region License
/*
 * ArgumentParser - Parsing class for command line arguments
 * Copyright (C) 2003 Sébastien Lorion
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

// http://www.codeproject.com/KB/recipes/argumentparser.aspx
// http://www.koders.com/csharp/fid0A5ED76069ACC45830FD56C7CB4BE2354F15F149.aspx?s=mdef%3Asearch
#endregion License

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;

namespace WgpgCore.ArgumentParser
{
    /// <summary>
    /// Represents a parsing class for command line arguments.
    /// </summary>
    public class ArgumentParser
    {
        /// <summary>
        /// Parses an array of arguments, typically obtained from Main method of console application.
        /// </summary>
        /// <param name="format">The valid format(s).</param>
        /// <param name="flags">Accepted flags (named or not) and their respective values.</param>
        /// <param name="ignoreCase">Indicates if parsing is case sensitive.</param>
        /// <param name="customPattern">An additional regular expression pattern to be used when parsing arguments. It will have priority over the standard pattern.</param>
        /// <remarks>In your pattern, use capture name constants made public by this class.</remarks>
        public ArgumentParser(ArgumentFormats format = ArgumentFormats.All, Boolean ignoreCase = false, StringDictionary flags = null, String customPattern = null)
        {
            HandledArguments = new StringDictionary();
            UnhandledArguments = new StringDictionary();
            ArgumentFormats = format;
            IgnoreCase = ignoreCase;
            Flags = flags;
            CustomPattern = customPattern;
            UseOnlyCustomPattern = false;
            AllowedPrefixes = new Char[] { '-', '/' };
            AssignSymbols = new Char[] { '=', ':' };
        }

        /// <summary>
        /// Parses an array of arguments, typically obtained from Main method of console application.
        /// </summary>
        /// <param name="customPattern">The custom pattern to be used.</param>
        /// <param name="useOnlyCustomPattern">Indicates if the custom pattern override the built-in pattern.</param>
        /// <remarks>In your pattern, use capture name constants made public by this class.</remarks>
        public ArgumentParser(String customPattern, Boolean useOnlyCustomPattern)
        {
            HandledArguments = new StringDictionary();
            UnhandledArguments = new StringDictionary();
            ArgumentFormats = ArgumentFormats.All;
            IgnoreCase = false;
            Flags = null;
            CustomPattern = customPattern;
            UseOnlyCustomPattern = useOnlyCustomPattern;
            AllowedPrefixes = null;
            AssignSymbols = null;
        }

        public const String FlagNameCaptureName = "flagName";
        public const String FlagsCaptureName = "flags";
        public const String PrefixCaptureName = "prefix";
        public const String ArgumentNameCaptureName = "name";
        public const String ArgumentValueCaptureName = "value";

        private StringDictionary Flags { get; set; }

        /// <summary>
        /// Gets the argument(s) that have been automatically set by AutoSetMembers method.
        /// </summary>
        public StringDictionary HandledArguments { get; private set; }

        /// <summary>
        /// Gets the argument(s) that have not been automatically set by AutoSetMembers method.
        /// </summary>
        public StringDictionary UnhandledArguments { get; private set; }

        /// <summary>
        /// Gets or sets the accepted argument format(s).
        /// </summary>
        public ArgumentFormats ArgumentFormats { get; set; }

        /// <summary>
        /// Gets or sets the accepted prefix(es).
        /// </summary>
        public Char[] AllowedPrefixes { get; set; }

        /// <summary>
        /// Gets or sets the assignation symbol(s).
        /// </summary>
        public Char[] AssignSymbols { get; set; }

        /// <summary>
        /// Gets or sets the case sensitive status.
        /// </summary>
        public Boolean IgnoreCase { get; set; }

        /// <summary>
        /// Gets or sets the custom pattern.
        /// </summary>
        public String CustomPattern { get; set; }

        /// <summary>
        /// Gets or sets the override status of the custom pattern.
        /// </summary>
        public Boolean UseOnlyCustomPattern { get; set; }

        /// <summary>
        /// Parses the array of arguments.
        /// </summary>
        /// <param name="args">The array of arguments.</param>
        /// <returns>The dictionary holding parsed arguments.</returns>
        public StringDictionary Parse(IList<String> args)
        {
            Int32 noNameCount = 0;
            Regex argex = BuildPattern();

            foreach (String arg in args)
            {
                Match match = argex.Match(arg);

                if (!match.Success)
                    throw new ArgumentException("Invalid argument format: " + arg);

                if (match.Groups[PrefixCaptureName].Success)
                {
                    if (match.Groups[FlagsCaptureName].Success)
                        UnhandledArguments.Add(match.Groups[FlagNameCaptureName].Value, match.Groups[FlagsCaptureName].Value);
                    else
                        UnhandledArguments.Add(match.Groups[ArgumentNameCaptureName].Value, match.Groups[ArgumentValueCaptureName].Value);
                }
                else
                {
                    UnhandledArguments.Add(noNameCount.ToString(CultureInfo.InvariantCulture), match.Groups[ArgumentValueCaptureName].Value);
                    noNameCount++;
                }
            }

            return UnhandledArguments;
        }

        /// <summary>
        /// Builds the pattern to be used when parsing each argument.
        /// </summary>
        /// <returns>A Regex instance to be used for parsing arguments.</returns>
        private Regex BuildPattern()
        {
            // The whole parsing string (with all possible argument formats) :
            // ---------------------------------------------------------------
            // (CUSTOM_PATTERN)
            // |(^(?<prefix>[PREFIXES])(?<flagName>)FLAG_NAMES)(?<flags>[FlagsCaptureName]+)$)
            // |(^(?<prefix>[PREFIXES])(?<name>[^EQUAL_SYMBOLS]+)([EQUAL_SYMBOLS](?<value>.+))?$)
            // |(LITERAL_STRING_SYMBOL?(?<value>.*))
            //
            // Again, but commented :
            // ----------------------
            // (CUSTOM_PATTERN)|				# custom pattern, if any (it has priority over standard pattern)
            //
            // foreach flag in FlagCollection :
            //
            // (^
            // (?<prefix>[PREFIXES])			# mandatory prefix
            // (?<flagName>)FLAG_NAMES)			# flag name
            // (?<flags>[FlagsCaptureName]+)				# flag value
            // $)|
            //
            // (^
            // (?<prefix>[PREFIXES])			# mandatory prefix
            // (?<name>[^EQUAL_SYMBOLS]+)		# argument name (which includes flag name/values)
            // ([EQUAL_SYMBOLS](?<value>.+))?	# argument value, if any
            // $)
            //
            // |(
            // LITERAL_STRING_SYMBOL?			# optional @ caracter indicating literal string
            // (?<value>.*)						# standalone value (will be given an index when parsed in Parse() method)
            // )

            RegexOptions regexOptions = RegexOptions.ExplicitCapture;

            if (IgnoreCase)
                regexOptions |= RegexOptions.IgnoreCase;

            if (UseOnlyCustomPattern)
                return new Regex(CustomPattern, regexOptions);

            StringBuilder argPattern = new StringBuilder();

            // build prefixes pattern
            StringBuilder prefixesBuilder = new StringBuilder();

            if (AllowedPrefixes != null && AllowedPrefixes.Length != 0)
            {
                prefixesBuilder.Append("(?<" + PrefixCaptureName + ">[");

                foreach (Char prefix in AllowedPrefixes)
                    prefixesBuilder.Append(prefix);

                prefixesBuilder.Append("])");
            }

            String prefixes = prefixesBuilder.ToString();

            // build custom pattern
            if (CustomPattern != null)
            {
                argPattern.Append('(');
                argPattern.Append(CustomPattern);
                argPattern.Append(")|");
            }

            // build flag pattern(s)
            if ((ArgumentFormats & ArgumentFormats.Flags) != 0)
            {
                foreach (DictionaryEntry flag in Flags)
                {
                    // pattern template : (^PREFIX_PATTERN(?<flagName>KEY)(?<flags>[ArgumentValueCaptureName]+))|
                    // eg. (^(?<prefix>[-/])(?<flagName>a)(?<flags>[ABCDEFG]+))|

                    argPattern.Append("(^");
                    argPattern.Append(prefixes);
                    argPattern.Append("(?<").Append(FlagNameCaptureName).Append(">");
                    argPattern.Append(flag.Key);
                    argPattern.Append(")(?<").Append(FlagsCaptureName).Append(">[");
                    argPattern.Append(flag.Value);
                    argPattern.Append("]+)$)|");
                }
            }

            // named arguments pattern
            argPattern.Append("(^");
            argPattern.Append(prefixes);

            if ((ArgumentFormats & ArgumentFormats.NamedValue) != 0)
            {
                argPattern.Append("(?<").Append(ArgumentNameCaptureName).Append(">[^");
                argPattern.Append(AssignSymbols);
                argPattern.Append("]+)");

                argPattern.Append("([");
                argPattern.Append(AssignSymbols);
                argPattern.Append("](?<").Append(ArgumentValueCaptureName).Append(">.+))?");
            }
            else
            {
                argPattern.Append("(?<").Append(ArgumentNameCaptureName).Append(">.+)");
            }

            argPattern.Append("$)");

            // standalone values
            argPattern.Append("|(@?(?<").Append(ArgumentValueCaptureName).Append(">.*))");

            return new Regex(argPattern.ToString(), regexOptions);
        }

        #region AutoSetMembers
        /// <summary>
        /// Automatically sets members for the provided <see cref="System.Reflection.Assembly"/>.
        /// </summary>
        /// <param name="assembly">The <see cref="System.Reflection.Assembly"/> to process.</param>
        public void AutoSetMembers(Assembly assembly)
        {
            Type[] types = assembly.GetTypes();

            foreach (Type type in types)
                AutoSetMembers(type);
        }

        /// <summary>
        /// Automatically sets members for the provided <see cref="System.Type"/>.
        /// </summary>
        /// <param name="type">The <see cref="System.Type"/> to process.</param>
        /// <remarks>Only static members will be processed.</remarks>
        public void AutoSetMembers(Type type)
        {
            MemberInfo[] members = type.FindMembers(AutoSetMemberAttribute.SupportedMemberTypes, AutoSetMemberAttribute.SupportedBindingFlags, Type.FilterName, "*");

            foreach (MemberInfo member in members)
                AutoSetMembers(type, member);
        }

        /// <summary>
        /// Automatically sets members for the provided class instance.
        /// </summary>
        /// <param name="instance">The class instance to process. Must not be null.</param>
        /// <remarks>Both static and instance members will be processed.</remarks>
        public void AutoSetMembers(Object instance)
        {
            MemberInfo[] members = instance.GetType().FindMembers(AutoSetMemberAttribute.SupportedMemberTypes, AutoSetMemberAttribute.SupportedBindingFlags, Type.FilterName, "*");

            foreach (MemberInfo member in members)
                AutoSetMembers(instance, member);
        }

        /// <summary>
        /// Automatically sets member of the provided class instance or <see cref="System.Type"/>.
        /// </summary>
        /// <param name="classToProcess">The class instance or <see cref="System.Type"/> to process.</param>
        /// <param name="member">The member which will be set. Must be a field or a property.</param>
        /// <remarks>Both static and instance members are accepted.</remarks>
        public void AutoSetMembers(Object classToProcess, MemberInfo member)
        {
            AutoSetMemberAttribute attrib = Attribute.GetCustomAttribute(member, typeof(AutoSetMemberAttribute)) as AutoSetMemberAttribute;

            if (attrib != null)
            {
                if (attrib.ResID != null && AutoSetMemberAttribute.Resources != null)
                    attrib.Aliases.Add(AutoSetMemberAttribute.Resources.GetString(attrib.ResID, AutoSetMemberAttribute.Culture));

                String argValue = null;
                bool found = false;

                foreach (String alias in attrib.Aliases)
                {
                    if (UnhandledArguments.ContainsKey(alias))
                    {
                        argValue = UnhandledArguments[alias];

                        HandledArguments.Add(alias, argValue);
                        UnhandledArguments.Remove(alias);

                        found = true;
                        break;
                    }

                    if (HandledArguments.ContainsKey(alias))
                    {
                        argValue = HandledArguments[alias];

                        found = true;
                        break;
                    }
                }

                if (found)
                {
                    Type memberType = null;

                    switch (member.MemberType)
                    {
                        case MemberTypes.Property:
                            memberType = ((PropertyInfo)member).PropertyType;
                            break;

                        case MemberTypes.Field:
                            memberType = ((FieldInfo)member).FieldType;
                            break;
                    }

                    if (memberType == typeof(Boolean))
                    {
                        if (String.IsNullOrEmpty(argValue))
                            SetMemberValue(classToProcess, member, !attrib.SwitchMeansFalse);
                        else
                        {
                            Boolean b;
                            if (Boolean.TryParse(argValue, out b))
                                SetMemberValue(classToProcess, member, b);
                            else
                            {
                                // last chance ... if can't parse it as integer, an exception will be raised
                                SetMemberValue(classToProcess, member, Int32.Parse(argValue) != 0);
                            }
                        }
                    }
                    else if (memberType == typeof(String))
                        SetMemberValue(classToProcess, member, argValue);
                    else if (memberType.IsEnum)
                    {
                        object value = Enum.Parse(memberType, argValue, IgnoreCase);
                        SetMemberValue(classToProcess, member, value);
                    }
                    else if (memberType.IsValueType)
                        SetMemberValue(classToProcess, member, Convert.ChangeType(argValue, memberType));
                }
            }
        }

        /// <summary>
        /// Sets the static or instance member (property or field) to the specified value.
        /// </summary>
        /// <param name="instance">The class instance or <see cref="System.Type"/> to be used.</param>
        /// <param name="memberInfo">The member to be set.</param>
        /// <param name="value">The new value of the member.</param>
        private static void SetMemberValue(Object instance, MemberInfo memberInfo, Object value)
        {
            if (memberInfo is PropertyInfo)
            {
                PropertyInfo pi = (PropertyInfo)memberInfo;

                if (pi.CanWrite)
                {
                    MethodInfo methodInfo = pi.GetSetMethod(true);

                    BindingFlags bindingFlags = BindingFlags.SetProperty;

                    if (methodInfo.IsStatic)
                        bindingFlags |= BindingFlags.Static;

                    pi.SetValue(instance, value, bindingFlags, null, null, null);
                }
            }
            else if (memberInfo is FieldInfo)
            {
                FieldInfo fi = (FieldInfo)memberInfo;

                BindingFlags bindingFlags = BindingFlags.SetField;

                if (fi.IsStatic)
                    bindingFlags |= BindingFlags.Static;

                fi.SetValue(instance, value, bindingFlags, null, null);
            }
        }
        #endregion

        /// <summary>
        /// Clears all saved arguments (both handled and unhandled).
        /// </summary>
        public void Clear()
        {
            HandledArguments.Clear();
            UnhandledArguments.Clear();
        }
    }
}
